package hotboxr::Auth::Store;
use strict; use warnings;
use base qw/Class::Accessor::Fast/;

BEGIN {
    __PACKAGE__->mk_accessors(qw/_config realm/);
}

sub new {
    my ($class,$config,$app,$realm) = @_;
    
    my $self = { _config => $config };
    bless $self, $class;
    $self->realm($realm) if $realm;

    return $self;
}

sub find_user {
    my ($self,$opts,$mongo,$crypt_key) = @_;
    my $user = hotboxr::Auth::Store::User->new(
        _mongo => $mongo,
        credentials => $opts,
        _crypt_key => $crypt_key
    );
    my $response;
    if ($opts->{logging_out}) { eval { $user->logout() }; }
    else { eval { $user->login(); } }
    
    if ($@) { warn "error: $@"; return undef; }
    return undef unless $user->response->success;
    return $user;
}

package hotboxr::Auth::Store::User;
use strict; use warnings;
use base qw/Catalyst::Authentication::User Class::Accessor::Fast/;
use Crypt::Rijndael;
use MIME::Base64;
use boolean;
use DateTime;

BEGIN {
    __PACKAGE__->mk_accessors(qw/ cipher id logging_out session_key credentials _mongo _user _crypt_key is_logged_out /);
}

sub new {
    my $class = shift;
    $class = ref($class) || $class;
    my %args = @_;
    my $self = {};
    bless($self,$class);
    while(my($a,$v) = each %args){
        if ($self->can($a)) { $self->$a($v); }
        else { die "unknown attribute: $a"; }
    }
    $self->cipher(Crypt::Rijndael->new( $self->_crypt_key, Crypt::Rijndael::MODE_CBC() )); 
    my $user_collection = $self->_mongo->c('users');
    $self->_user($user_collection->find_one({name => $self->credentials->{username}}));
    $self->id($self->_user->{_id}->to_string);
    $self;
}

sub get {
    my ($self,$field) = @_;
    return $self->_user->{$field};
}

sub get_object { shift; }

sub login {
    my ($self) = @_;
    $self->response($self->_auth_by_session());
    return 1 if $self->response->success;
    $self->response($self->_auth_by_credentials());
    return 1 if $self->response->success;
    return undef; 
}

sub logout {
    my ($self) = @_;
    $self->is_logged_out($self->terminate_all_sessions());
    $self->response(hotboxr::Auth::Store::Response->new(
        success => $self->is_logged_out ? 1 : 0 
    ));
}

sub _auth_by_session {
    my ($self) = @_;
    return hotboxr::Auth::Store::Response->new(success => 0, reason => 'no session key') if !$self->credentials->{encrypted_session_key};
    $self->uncrypt_session();
    my $session = $self->_user->{sessions}->find_one({key => $self->session_key, active => boolean::true(), ip_address => $self->credentials->{ip_address}});
    $self->session($session);
    return hotboxr::Auth::Store::Response->new((!defined $self->session ? (success => 0, reason => 'no active session') :  (success => 1)));
}

sub _auth_by_credentials {
    my ($self) = @_;
    for (qw/username password/) { 
        return hotboxr::Auth::Store::Response->new( success => 0, reason => 'missing ' . $_ ) if !$self->credentials->{$_};
    }
    if ($self->_check_password()) {
        $self->create_new_session();
        return hotboxr::Auth::Store::Response->new( success => 1 );
    } else { return hotboxr::Auth::Store::Response->new( success => 0, reason => 'invalid password' ) } 
}

sub _check_password {
    my ($self) = @_;
    my $decrypted_stored = $self->cipher->decrypt($self->_user->{password});
    return 1 if $self->credentials->{password} eq $decrypted_stored;
    return 0;
}

sub create_new_session {
    my ($self) = @_;
    $self->terminate_all_sessions();
    $self->session_key(
        $self->_n_bytes_in_base64(32)
    );
    $self->encrypt_session();
    push @{$self->_user->{sessions}}, {
        key => $self->credentials->{encrypted_session_key},
        active => boolean::true(),
        started => DateTime->now,
        ip_address => $self->credentials->{ip_address}
    };
    my $collection = $self->_mongo->c('users');
    $collection->save($self->_user);
}

sub _n_bytes_in_base64 {
    my ($self,$n) = @_;
    if (open(R,"/dev/random")) {
        my $b;
        read(R,$b,$n);
        return encode_base64($b);
    }
    else { die "cannot read from /dev/urandom" }
}

sub uncrypt_session {
    my ($self) = @_;
    my $decrypted_key = $self->cipher->decrypt($self->credentials->{encrypted_session_key});
    return $self->session_key($decrypted_key);
}

sub encrypt_session {
    my ($self) = @_;
    my $encrypted_session = $self->cipher->encrypt($self->session_key());
    return $self->credentials->{encrypted_session_key} = $encrypted_session; 
}

sub terminate_all_sessions {
    my ($self) = @_;
    foreach my $s (@$self->_user->{sessions}) { $s->{active} = 0; }
    my $collection = $self->_mongo->c('users');
    return $collection->save($self->_user); #should always be an upsert, thus returns 1.
}

#############################################
package hotboxr::Auth::Store::Response;
use strict; use warnings;
use base qw/Class::Accessor::Fast/;
use MongoDB;

BEGIN {
    __PACKAGE__->mk_accessors(qw/success reason/);
}

sub new {
    my $class = shift;
    $class = ref($class) || $class;
    my %args = @_;
    my $self = {};
    bless($self,$class);
    while(my($a,$v) = each %args){
        if ($self->can($a)) { $self->$a($v); }
        else { die "unknown attribute: $a"; }
    }
    $self;
}


1;
